<?php

/**
 * Signal-Slot
 *
 * @author Mark A. Prisyazhnyuk <nixmrak@meta.ua>
 */
class SignalSlot {

    protected $aSignals;
    /**
     * @var array aSlotsBySignal signal-slots pairs
     *
     * 	array(
     * 	'signal1'=>array(
     * 	    0=>array($slot1, $slot2),
     * 	    10=>array($slotFoo, $slotBar)
     * 	),
     * 	'signal2'=>array(
     * 	    0=>array($slot24, $slot73),
     * 	    10=>array($slotBaz, $slotBah)
     * 	),
     * )
     */
    protected $aSlotsBySignal;

    /**
     * @var SignalSlot Синглтон-экземпляр
     */
    protected static $_instance;

    protected function __construct() {
	
    }

    /**
     * Получить синглтон-экземпляр
     * @return SignalSlot
     */
    public static function getInstance() {

	if (null == self::$_instance) {
	    self::$_instance = new self();
	}
	return self::$_instance;
    }

    /**
     * Добавить сигнал
     *
     * @param string $sSignal 
     */
    public function addSignal($sSignal) {
	$signals = & self::getInstance()->aSignals;
	$sSignal = (string) $sSignal;

	if (!@in_array($sSignal, $signals)) {
	    $signals[] = $sSignal;
	}
    }

    /**
     * Добавить слот.
     * 
     * Подписывает слот $slot на сигнал $sSignal с приоритетом $iPriority
     * Слоты с низшим приоритетом запускаются раньше.
     * Слоты с одним приоритетом запускаются в порядке их подписки
     * 
     * @param string	$sSignal    сигнал
     * @param callback	$slot	    слот
     * @param integer	$iPriority  приоритет
     * 
     */
    public function addSlot($sSignal, $slot, $iPriority=0) {

	$iPriority = (int) $iPriority;

	if (!in_array($sSignal, self::getInstance()->aSignals)) {
	    throw new Exception('Unknown signal');
	}

	if (!is_callable($slot)) {
	    throw new Exception('Slot is not callable');
	}

	$slotsBySignal = & self::getInstance()->aSlotsBySignal;
	$slotsBySignal[$sSignal][$iPriority][] = $slot;

	ksort($slotsBySignal[$sSignal]);
    }

    /**
     * Послать сигнал
     * 
     * При посылке сигнала $sSignal, вызываются в соответствии с приоритетом все
     * подписанные на этот сигнал слоты (если они еще существуют на данный
     * момент). Каждому слоту передается по ссылке аргумент $aParams
     * (в слот-функциях обязательно пишите амперсанд перед аргументом!).
     * 
     * Слоты могут поочередно вносить изменения в аргумент.
     * 
     * Слоты могут возвращать значения. Если слот вернул не null, его
     * возвращенное значение добавляется к массиву, который затем возвращается
     * функцией emit.
     * 
     * Если слот, подписанный на данное событие, перестал существовать,
     * его вызов просто пропускается (без генерации исключения).
     * 
     * @param string $sSignal
     * @param mixed $aParams
     * @return array
     */
    public function emit($sSignal, &$aParams=array()) {

	$slots = & self::getInstance()->aSlotsBySignal;
	$aReturn = array();

	if (is_array(@$slots[$sSignal])) {
	    foreach ($slots[$sSignal] as $priority => &$slotGroup) {
		//walking through slot groups of each priority
		if (is_array($slotGroup)) {
		    foreach ($slotGroup as &$slot) {
			//walking throught slots in slot group of the given priority
			if (is_callable($slot)) { //calling slot
				$bFinal = false;
			    $retVal = call_user_func($slot,&$aParams, &$bFinal);
			    if (null !== $retVal) {
				$aReturn []=$retVal;
			    }
			    if($bFinal) {break;}
			}
		    }
		}
		if($bFinal) {break;}
	    }
	}
	return $aReturn;
    }

}
